﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using System.CodeDom.Compiler;
using System.Reflection;
using TrinityAPI.Base.Models;

namespace TrinityAPI.Base.Tools
{
    public static class LinqHelper
    {
        public static JToken GetLinqData(string path, JObject dataset)
        {
            if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(APIContext.ApiFullName))
                return null;

            var linqId = path;
            var index = -1;
            var keys = path.Split('[');
            if (keys.Length == 2)
            {
                linqId = keys[0];
                string num = keys[1].TrimEnd(']');
                if (!int.TryParse(num, out index))
                    index = -1;
            }

            var linqs = dataset.GetJsonObject("linqs");
            var linqContent = linqs.GetJsonObjectValue(linqId);
            if (linqContent == null || (linqContent.Type != JTokenType.Object && linqContent.Type != JTokenType.Array))
                return null;

            string subId = string.Empty;
            JObject linqContentObject = null;
            List<LinqParameter> parameters = null;
            if (linqContent.Type == JTokenType.Object)
            {
                linqContentObject = (linqContent as JObject);
                parameters = linqContentObject.GetJsonObjectList<LinqParameter>("params");
            }
            else if (linqContent.Type == JTokenType.Array)
            {
                if (index < 0)
                    return null;
                var linqContentList = linqContent as JArray;
                if (linqContentList.Count < index + 1)
                    return null;
                linqContentObject = (linqContentList[index] as JObject);
                subId = "_" + linqContentObject.GetJsonObjectStringValue("index");
                parameters = linqContentObject.GetJsonObjectList<LinqParameter>("params");
            }
            if (linqContentObject.GetJsonObjectBooleanValue("inprogress")) //防止交叉引用而引起死循环
                return null;
            linqContentObject.SetJsonObjectValue("inprogress", true);
            var linqDataset = new JObject();
            if (parameters != null && parameters.Count > 0)
            {
                foreach (var linqParameter in parameters)
                {
                    linqParameter.Value = Utils.GetParameter(linqParameter.ParameterPath, dataset, linqParameter.Functions).Evaluate();
                    linqDataset.Add(linqParameter.ParameterName, linqParameter.Value);
                }
            }
            linqContentObject.SetJsonObjectValue("inprogress", false);

            var cType = Type.GetType(string.Format("TrinityAPI.Linq.LinqHelper,TrinityAPI.Linq.{0}", APIContext.ApiFullName.ToLower()), false, true);
            if (cType != null)
            {
                var obj = Activator.CreateInstance(cType);
                if (obj != null)
                {
                    var method = cType.GetMethod("DataFilter_" + linqId.ToLower() + subId, BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static);
                    if (method != null)
                    {
                        try
                        {
                            var data = method.Invoke(obj, new object[] { linqDataset });
                            return LinqHelper.ToJToken(data);
                        }
                        catch (TargetInvocationException ex)
                        {
                            throw new Exception("Get linq data error: " + ex.InnerException.Message);
                        }
                        catch (Exception ex)
                        {
                            throw new Exception("Get linq data error: " + ex.Message);
                        }
                    }
                }
            }

            return null;
        }

        public static void BuildLinq(JObject linqs)
        {
            if (!CodeDomProvider.IsDefinedLanguage("CSharp"))
                return;
            if (linqs == null || !linqs.HasValues)
                return;
            if (string.IsNullOrWhiteSpace(APIContext.ApiFullName))
                return;

            var cType = Type.GetType(string.Format("TrinityAPI.Linq.LinqHelper,TrinityAPI.Linq.{0}", APIContext.ApiFullName.ToLower()), false, true);
            if (cType != null)
            {
                var assemblyAttribute = cType.Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>();
                if (assemblyAttribute != null && !string.IsNullOrWhiteSpace(assemblyAttribute.Description))
                {
                    DateTime modTime = DateTime.MinValue;
                    if (DateTime.TryParse(assemblyAttribute.Description, out modTime) && (APIContext.ConfigLastModTime - modTime).TotalMilliseconds < 1)
                    {
                        return; //dll已经存在，并且记录的最后修改时间和配置文件的最后修改时间一致，直接返回，不再重新编译。
                    }
                }
            }

            var list = new JObject();
            foreach (var item in linqs)
            {
                var linqValue = linqs.GetJsonObjectValue(item.Key);
                if (linqValue == null || (linqValue.Type != JTokenType.Array && linqValue.Type != JTokenType.String))
                    continue;

                JArray newValueList = new JArray();
                JArray valueList = linqValue.Type == JTokenType.Array ? linqValue as JArray : new JArray() { linqValue };

                foreach (var value in valueList)
                {
                    if (value.Type != JTokenType.String)
                        continue;
                    var linq = value.ToString();
                    if (string.IsNullOrWhiteSpace(linq))
                        continue;
                    var indexAdjustment = 0;
                    var matches2 = Regex.Matches(linq, @"([@\!]?[a-zA-Z0-9_]+(\[[0-9]+\])?)(\.[@\!]?[a-zA-Z0-9_]+(\[[0-9]+\]|\(.*?\))?)+");
                    foreach (Match m in matches2)
                    {
                        Capture capture = m.Groups[0];
                        int index = capture.Index + indexAdjustment;
                        int length = capture.Length;
                        int firstdot = capture.Value.IndexOf(".");
                        string section = capture.Value;
                        string fist = section.Substring(0, firstdot);
                        string last = section.Substring(firstdot);
                        string replaced = Regex.Replace(last, @"([@\!]?[a-zA-Z0-9_]+)(\[[0-9]+\])?\.", @"GetLinqValue(""$1"")$2.");
                        replaced = Regex.Replace(replaced, @"([@\!]?[a-zA-Z0-9_]+)(\[[0-9]+\])?$", @"GetLinqValue(""$1"")$2");
                        replaced = fist + replaced;
                        linq = string.Format("{0}{1}{2}", linq.Substring(0, index), replaced, linq.Substring(index + length));
                        indexAdjustment += replaced.Length - section.Length;
                    }
                    newValueList.Add(linq);
                }
                if (!newValueList.HasValues)
                    continue;

                list[item.Key] = linqValue.Type == JTokenType.Array ? newValueList : newValueList[0];
            }

            string AssemblyDir = Utils.GetMapPath(string.Format(@"~/bin/Linqs/"));
            if (!Directory.Exists(AssemblyDir))
            {
                Directory.CreateDirectory(AssemblyDir);
            }
            string AssemblyPath = string.Format("{0}\\TrinityAPI.Linq.{1}.dll", AssemblyDir, APIContext.ApiFullName.ToLower());

            CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
            // 3.CompilerParameters
            CompilerParameters objCompilerParameters = new CompilerParameters();
            objCompilerParameters.ReferencedAssemblies.Add("System.dll");
            objCompilerParameters.ReferencedAssemblies.Add("System.Core.dll");
            objCompilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            objCompilerParameters.ReferencedAssemblies.Add(System.Web.Hosting.HostingEnvironment.MapPath(@"~/bin/TrinityAPI.dll"));
            objCompilerParameters.ReferencedAssemblies.Add(System.Web.Hosting.HostingEnvironment.MapPath(@"~/bin/TrinityAPI.Base.dll"));
            objCompilerParameters.ReferencedAssemblies.Add(System.Web.Hosting.HostingEnvironment.MapPath(@"~/bin/Newtonsoft.Json.dll"));
            objCompilerParameters.GenerateExecutable = false;
            objCompilerParameters.GenerateInMemory = false;
            objCompilerParameters.OutputAssembly = AssemblyPath;

            // 4.CompilerResults

            CompilerResults cr = provider.CompileAssemblyFromSource(objCompilerParameters, GenerateCode(list, APIContext.ConfigLastModTime));

            if (cr.Errors.HasErrors)
            {
                foreach (CompilerError err in cr.Errors)
                {
                    throw new ConfigException("Linq dynamic compilation error: " + err.ErrorText);
                }
            }
        }

        private static string GenerateCode(JObject linqs, DateTime configModTime)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("using System;");
            sb.AppendLine("using System.Collections.Generic;");
            sb.AppendLine("using System.Linq;");
            sb.AppendLine("using Newtonsoft.Json.Linq;");
            sb.AppendLine("using TrinityAPI.Tools;");
            sb.AppendLine("using TrinityAPI.Base.Tools;");
            sb.AppendLine("using TrinityAPI.Base.Models;");
            sb.AppendLine("using System.Reflection;");
            sb.AppendLine("[assembly: AssemblyDescription(\"" + configModTime.ToString("yyyy-MM-dd HH:mm:ss.fff") + "\")]");
            sb.Append(Environment.NewLine);
            sb.AppendLine("namespace TrinityAPI.Linq");
            sb.AppendLine("{");
            sb.AppendLine("    public class LinqHelper");
            sb.AppendLine("    {");
            foreach (var item in linqs)
            {
                if (item.Value.Type != JTokenType.Array && item.Value.Type != JTokenType.String)
                    continue;

                if (item.Value.Type == JTokenType.Array)
                {
                    var index = 0;
                    foreach(var value in item.Value as JArray)
                    {
                        sb.AppendLine("        public object DataFilter_" + item.Key.ToLower() + "_" + index++ + "(JObject dataset)");
                        sb.AppendLine("        {");
                        sb.AppendLine("             var resp = " + value + ";");
                        sb.AppendLine("             return resp;");
                        sb.AppendLine("        }");
                    }
                }
                else
                {
                    sb.AppendLine("        public object DataFilter_" + item.Key.ToLower() + "(JObject dataset)");
                    sb.AppendLine("        {");
                    sb.AppendLine("             var resp = " + item.Value + ";");
                    sb.AppendLine("             return resp;");
                    sb.AppendLine("        }");
                }
            }
            sb.AppendLine("    }");
            sb.AppendLine("}");

            string code = sb.ToString();

            return code;
        }

        public static LinqValue GetLinqValue(this object obj, string key)
        {
            if (obj != null)
            {
                JToken JObj = ToJToken(obj);
                if (JObj is JObject)
                {
                    JToken value = (JObj as JObject).GetJsonObjectValue(key);
                    if (value != null)
                    {
                        return new LinqValue(value);
                    }
                }
            }
            return new LinqValue(null);
        }

        public static JToken ToJToken(object obj)
        {
            if (obj == null || obj is Microsoft.ClearScript.Undefined)
                return null;

            Type t = obj.GetType();
            if (t == typeof(JToken) || t.IsSubclassOf(typeof(JToken)))
                return (JToken)obj;

            PrimitiveTypeCode typeCode = ConvertUtils.GetTypeCode(t);
            if (typeCode != PrimitiveTypeCode.Empty && typeCode != PrimitiveTypeCode.Object)
                return new JValue(obj);

            if (t == typeof(LinqValue) || t.IsSubclassOf(typeof(LinqValue)))
            {
                return ((LinqValue)obj).Value<JToken>();
            }

            if (typeof(IDictionary).IsAssignableFrom(t) || ConvertUtils.ImplementsGenericDefinition(t, typeof(IDictionary<,>)) || ConvertUtils.ImplementsGenericDefinition(t, typeof(IReadOnlyDictionary<,>)))
            {
                if (obj is IDictionary)
                    return GetDictionary((IDictionary)obj);
                else
                    return JToken.FromObject(obj);
            }

            if (typeof(IEnumerable).IsAssignableFrom(t))
            {
                if ((ConvertUtils.IsNullableType(t) ? Nullable.GetUnderlyingType(t) : t).IsArray && t.GetArrayRank() > 1) //多维数组
                {
                    return GetMultidimensionalArray((Array)obj, 0, new int[0]);
                }
                else
                {
                    JArray jarr = new JArray();
                    foreach (object value in (IEnumerable)obj)
                    {
                        jarr.Add(ToJToken(value));
                    }
                    return jarr;
                }
            }

            if (ConvertUtils.CanConvertToString(t))
            {
                string str;
                ConvertUtils.TryConvertToString(obj, t, out str);
                return str;
            }

            if (typeof(System.Dynamic.IDynamicMetaObjectProvider).IsAssignableFrom(t))
            {
                return JToken.FromObject(obj);
            }

            if (typeCode == PrimitiveTypeCode.Object)
            {
                JObject jobj = new JObject();
                var properties = obj.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).ToList();
                foreach (var property in properties)
                {
                    var value = property.GetValue(obj, null);
                    jobj[property.Name] = ToJToken(value);
                }
                return jobj;
            }

            return null;
        }

        private static JToken GetDictionary(IDictionary values)
        {
            var jobj = new JObject();
            IDictionaryEnumerator e = values.GetEnumerator();
            try
            {
                while (e.MoveNext())
                {
                    DictionaryEntry entry = e.Entry;

                    PrimitiveTypeCode typeCode = ConvertUtils.GetTypeCode(entry.Key.GetType());
                    string propertyName = GetPropertyName(entry.Key, typeCode);

                    object value = entry.Value;
                    jobj[propertyName] = ToJToken(value);
                }
            }
            finally
            {
                (e as IDisposable)?.Dispose();
            }
            return jobj;
        }

        private static string GetPropertyName(object name, PrimitiveTypeCode typeCode)
        {
            string propertyName;
            if (typeCode != PrimitiveTypeCode.Empty && typeCode != PrimitiveTypeCode.Object)
            {
                switch (typeCode)
                {
                    case PrimitiveTypeCode.DateTime:
                    case PrimitiveTypeCode.DateTimeNullable:
                        {
                            DateTime dt = DateTimeUtils.EnsureDateTime((DateTime)name, Newtonsoft.Json.DateTimeZoneHandling.RoundtripKind);

                            System.IO.StringWriter sw = new System.IO.StringWriter(CultureInfo.InvariantCulture);
                            DateTimeUtils.WriteDateTimeString(sw, dt, Newtonsoft.Json.DateFormatHandling.IsoDateFormat, null, CultureInfo.InvariantCulture);
                            return sw.ToString();
                        }
                    case PrimitiveTypeCode.DateTimeOffset:
                    case PrimitiveTypeCode.DateTimeOffsetNullable:
                        {
                            System.IO.StringWriter sw = new System.IO.StringWriter(CultureInfo.InvariantCulture);
                            DateTimeUtils.WriteDateTimeOffsetString(sw, (DateTimeOffset)name, Newtonsoft.Json.DateFormatHandling.IsoDateFormat, null, CultureInfo.InvariantCulture);
                            return sw.ToString();
                        }
                    case PrimitiveTypeCode.Double:
                    case PrimitiveTypeCode.DoubleNullable:
                        {
                            double d = (double)name;
                            return d.ToString("R", CultureInfo.InvariantCulture);
                        }
                    case PrimitiveTypeCode.Single:
                    case PrimitiveTypeCode.SingleNullable:
                        {
                            float f = (float)name;
                            return f.ToString("R", CultureInfo.InvariantCulture);
                        }
                    default:
                        {
                            return Convert.ToString(name, CultureInfo.InvariantCulture);
                        }
                }
            }
            else if (name is LinqValue)
            {
                propertyName = ((LinqValue)name).Value<JToken>()?.ToString();
                return propertyName ?? string.Empty;
            }
            else if (ConvertUtils.TryConvertToString(name, name.GetType(), out propertyName))
            {
                return propertyName;
            }
            else
            {
                return name.ToString();
            }
        }

        private static JToken GetMultidimensionalArray(Array values, int initialDepth, int[] indices)
        {
            int dimension = indices.Length;
            int[] newIndices = new int[dimension + 1];
            for (int i = 0; i < dimension; i++)
            {
                newIndices[i] = indices[i];
            }
            JArray arr = new JArray();
            for (int i = values.GetLowerBound(dimension); i <= values.GetUpperBound(dimension); i++)
            {
                newIndices[dimension] = i;
                bool isTopLevel = (newIndices.Length == values.Rank);

                if (isTopLevel)
                {
                    object value = values.GetValue(newIndices);

                    arr.Add(ToJToken(value));
                }
                else
                {
                    arr.Add(GetMultidimensionalArray(values, initialDepth + 1, newIndices));
                }
            }
            return arr;
        }
    }
}